{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "view-in-github"
   },
   "source": [
    "<a href=\"https://colab.research.google.com/github/tomasonjo/blogs/blob/master/llm/text_embedding_limitations.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "id": "CieeAMvNi_1I"
   },
   "outputs": [],
   "source": [
    "!pip install --quiet neo4j langchain-community langchain-openai langgraph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "id": "4Md8CEsJjFuT"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import re\n",
    "from langchain_community.graphs import Neo4jGraph\n",
    "from langchain_community.vectorstores import Neo4jVector\n",
    "from langchain_openai import OpenAIEmbeddings, ChatOpenAI\n",
    "import getpass\n",
    "from pydantic import BaseModel, Field\n",
    "from typing import Optional, Dict, List\n",
    "from langchain_core.tools import tool\n",
    "\n",
    "from langgraph.graph import START, StateGraph\n",
    "from langgraph.prebuilt import tools_condition\n",
    "from langgraph.prebuilt import ToolNode\n",
    "from IPython.display import Image, display\n",
    "\n",
    "from langgraph.graph import MessagesState\n",
    "from langchain_core.messages import HumanMessage, SystemMessage"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Limitations of text embeddings in RAG applications\n",
    "## And how to overcome them using knowledge graphs and structured tools\n",
    "\n",
    "Everyone loves text embedding models, and for good reason — they excel at encoding unstructured text, making it easier to discover semantically similar content. It’s no surprise that they form the backbone of most RAG applications, especially with the current emphasis on encoding and retrieving relevant information from documents and other textual resources. However, there are clear examples of questions one might ask where text embedding approach to RAG applications falls short and delivers incorrect information.\n",
    "\n",
    "As mentioned, text embeddings are great at encoding unstructured text. On the other hand, they aren’t that great at dealing with structured information and operations such as filtering, sorting, or aggregations. Imagine a simple question like:\n",
    "\n",
    "_What is the highest-rated movie released in 2024?_\n",
    "\n",
    "To answer this question, we must first filter by release year, followed by sorting by rating. We’ll examine how a naive approach with text embeddings performs and then demonstrate how to deal with such questions. This blog post showcases that when dealing with structured data operations such as sorting, filtering, or aggregating, you need to use tools different from text embeddings.\n",
    "\n",
    "The code is available on GitHub.\n",
    "\n",
    "## Environment setup\n",
    "In this blog post, we will use the [recommendations project in Neo4j Sandbox](https://sandbox.neo4j.com/?usecase=recommendations). The recommendations project uses the MovieLens dataset, which contains movies, actors, ratings, and more information.\n",
    "\n",
    "The following code will instantiate a LangChain wrapper to connect to Neo4j Database."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "id": "NQFdqemqlIE2"
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/tomazbratanic/anaconda3/lib/python3.11/site-packages/pandas/core/arrays/masked.py:60: UserWarning: Pandas requires version '1.3.6' or newer of 'bottleneck' (version '1.3.5' currently installed).\n",
      "  from pandas.core import (\n"
     ]
    }
   ],
   "source": [
    "os.environ[\"NEO4J_URI\"] = \"bolt://52.70.37.100:7687\"\n",
    "os.environ[\"NEO4J_USERNAME\"] = \"neo4j\"\n",
    "os.environ[\"NEO4J_PASSWORD\"] = \"challenge-armament-recoveries\"\n",
    "\n",
    "graph = Neo4jGraph(refresh_schema=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Additionally, you will require an OpenAI api key that you pass in the following code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "NEoFQzJFlPbL",
    "outputId": "9e7454bf-2540-43ca-b9d8-efea87a03004"
   },
   "outputs": [
    {
     "name": "stdin",
     "output_type": "stream",
     "text": [
      "OpenAI API Key: ········\n"
     ]
    }
   ],
   "source": [
    "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The database contains 10,000 movies, but text embeddings are not yet stored. To avoid calculating embeddings for all of them, we’ll tag the 1000 top-rated films with a secondary label called Target."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "eZG9r4hcje2K",
    "outputId": "74aa5fcc-896a-487f-89c1-de323b5c1283"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[]"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "graph.query(\"\"\"\n",
    "MATCH (m:Movie)\n",
    "WHERE m.imdbRating IS NOT NULL\n",
    "WITH m\n",
    "ORDER BY m.imdbRating DESC\n",
    "LIMIT 1000\n",
    "SET m:Target\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "QApUVcEEkZkN",
    "outputId": "0c95c267-f7ad-42af-d0f8-9a5b49d91e3b"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'m': {'budget': 30000000,\n",
       "   'movieId': '1',\n",
       "   'tmdbId': '862',\n",
       "   'imdbVotes': 591836,\n",
       "   'runtime': 81,\n",
       "   'countries': ['USA'],\n",
       "   'imdbId': '0114709',\n",
       "   'url': 'https://themoviedb.org/movie/862',\n",
       "   'plot': \"A cowboy doll is profoundly threatened and jealous when a new spaceman figure supplants him as top toy in a boy's room.\",\n",
       "   'released': '1995-11-22',\n",
       "   'languages': ['English'],\n",
       "   'imdbRating': 8.3,\n",
       "   'title': 'Toy Story',\n",
       "   'poster': 'https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg',\n",
       "   'year': 1995,\n",
       "   'embedding': None,\n",
       "   'revenue': 373554033}}]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "graph.query(\"\"\"MATCH (m:Target) RETURN m {.*, embedding: Null} LIMIT 1\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calculating and storing text embeddings\n",
    "Deciding what to embed is an important consideration. Since we’ll be demonstrating filtering by year and sorting by rating, it wouldn’t be fair to exclude those details from the embedded text. That’s why, in this case, I chose to capture the release year, rating, title, and description of each movie.\n",
    "\n",
    "Here is an example text we will embed for the Wolf of Wall Street movie.\n",
    "```\n",
    "plot: Based on the true story of Jordan Belfort, from his rise to a wealthy \n",
    "      stock-broker living the high life to his fall involving crime, corruption\n",
    "      and the federal government.\n",
    "title: Wolf of Wall Street, The\n",
    "year: 2013\n",
    "imdbRating: 8.2\n",
    "```\n",
    "You might say this is not a good approach to embedding structured data, and I wouldn’t argue as I don’t know the best approach. Maybe instead of key-value items, we should convert them to text or something. Let me know if you have some ideas about what might work better.\n",
    "\n",
    "The Neo4jVector object in LangChain has a convenient method `from_existing_graph` where you can select which text properties should be encoded."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "id": "v8kOES4mkEpw"
   },
   "outputs": [],
   "source": [
    "embedding = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
    "\n",
    "neo4j_vector = Neo4jVector.from_existing_graph(\n",
    "    embedding=embedding,\n",
    "    index_name=\"movies\",\n",
    "    node_label=\"Target\",\n",
    "    text_node_properties=[\"plot\", \"title\", \"year\", \"imdbRating\"],\n",
    "    embedding_node_property=\"embedding\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we utilize OpenAI’s text-embedding-3-small model for embedding generation. We initialize the Neo4jVector object using the from_existing_graph method. The node_label parameter filters the nodes to be encoded, specifically those labeled Target. The text_node_properties parameter defines the node properties to be embedded, including plot, title, year, and imdbRating. Finally, the embedding_node_property defines the property where the generated embeddings will be stored, here designated as embedding."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pretty_print(vector_results):\n",
    "    for result in vector_results:\n",
    "        print(result.page_content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The naive approach\n",
    "Let’s start by trying to find a movie based on its plot or description!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "lMZsuToClkqc",
    "outputId": "00c297c6-457a-4423-ec7c-92b0ede7c01b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "plot: A young boy befriends a giant robot from outer space that a paranoid government agent wants to destroy.\n",
      "title: Iron Giant, The\n",
      "year: 1999\n",
      "imdbRating: 8.0\n",
      "\n",
      "plot: After the death of a friend, a writer recounts a boyhood journey to find the body of a missing boy.\n",
      "title: Stand by Me\n",
      "year: 1986\n",
      "imdbRating: 8.1\n",
      "\n",
      "plot: A young, naive boy sets out alone on the road to find his wayward mother. Soon he finds an unlikely protector in a crotchety man and the two have a series of unexpected adventures along the way.\n",
      "title: Kikujiro (Kikujirô no natsu)\n",
      "year: 1999\n",
      "imdbRating: 7.9\n",
      "\n",
      "plot: While home sick in bed, a young boy's grandfather reads him a story called The Princess Bride.\n",
      "title: Princess Bride, The\n",
      "year: 1987\n",
      "imdbRating: 8.1\n"
     ]
    }
   ],
   "source": [
    "pretty_print(\n",
    "    neo4j_vector.similarity_search(\n",
    "        \"What is a movie where a little boy meets his hero?\"\n",
    "    )\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The results seem pretty solid overall. There’s consistently a little boy involved, though I’m not sure if he always meets his hero. Then again, the dataset only includes 1,000 movies, so the options are somewhat limited.\n",
    "\n",
    "Now let’s try a query that requires some basic filtering."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "ofVsXM3rmUTy",
    "outputId": "1883a8c2-4e63-474f-de6e-eee6f1f2dea8"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "plot: Six short stories that explore the extremities of human behavior involving people in distress.\n",
      "title: Wild Tales\n",
      "year: 2014\n",
      "imdbRating: 8.1\n",
      "\n",
      "plot: A young man who survives a disaster at sea is hurtled into an epic journey of adventure and discovery. While cast away, he forms an unexpected connection with another survivor: a fearsome Bengal tiger.\n",
      "title: Life of Pi\n",
      "year: 2012\n",
      "imdbRating: 8.0\n",
      "\n",
      "plot: Based on the true story of Jordan Belfort, from his rise to a wealthy stock-broker living the high life to his fall involving crime, corruption and the federal government.\n",
      "title: Wolf of Wall Street, The\n",
      "year: 2013\n",
      "imdbRating: 8.2\n",
      "\n",
      "plot: After young Riley is uprooted from her Midwest life and moved to San Francisco, her emotions - Joy, Fear, Anger, Disgust and Sadness - conflict on how best to navigate a new city, house, and school.\n",
      "title: Inside Out\n",
      "year: 2015\n",
      "imdbRating: 8.3\n"
     ]
    }
   ],
   "source": [
    "pretty_print(\n",
    "    neo4j_vector.similarity_search(\n",
    "        \"Which movies are from year 2016?\"\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It’s funny, but not a single movie from 2016 was selected. Maybe we could get better results with different text preparation for encoding. However, text embeddings aren’t applicable here as we are dealing with a simple structured data operation where we need to filter documents, or, in this example, movies, based on a metadata property. Metadata filtering is a well-established technique, often employed to enhance the accuracy of RAG systems.\n",
    "\n",
    "The next query we will try requires a bit of sorting."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "0zzQogWclzvi",
    "outputId": "be1f0279-964b-48c9-8dc3-faca26adfdd0"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "plot: A silent film production company and cast make a difficult transition to sound.\n",
      "title: Singin' in the Rain\n",
      "year: 1952\n",
      "imdbRating: 8.3\n",
      "\n",
      "plot: A film about the greatest pre-Woodstock rock music festival.\n",
      "title: Monterey Pop\n",
      "year: 1968\n",
      "imdbRating: 8.1\n",
      "\n",
      "plot: This movie documents the Apollo missions perhaps the most definitively of any movie under two hours. Al Reinert watched all the footage shot during the missions--over 6,000,000 feet of it, ...\n",
      "title: For All Mankind\n",
      "year: 1989\n",
      "imdbRating: 8.2\n",
      "\n",
      "plot: An unscrupulous movie producer uses an actress, a director and a writer to achieve success.\n",
      "title: Bad and the Beautiful, The\n",
      "year: 1952\n",
      "imdbRating: 7.9\n"
     ]
    }
   ],
   "source": [
    "pretty_print(\n",
    "    neo4j_vector.similarity_search(\"Which movie has the highest imdb score?\")\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "7NpvV2Plz2W5",
    "outputId": "9a19bd89-6601-483a-fd3f-a9be103cb3d9"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'title': 'Band of Brothers', 'rating': 9.6},\n",
       " {'title': 'Civil War, The', 'rating': 9.5},\n",
       " {'title': 'Shawshank Redemption, The', 'rating': 9.3},\n",
       " {'title': 'Cosmos', 'rating': 9.3}]"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "graph.query(\"\"\"MATCH (m:Target) RETURN m.title AS title, m.imdbRating AS rating ORDER BY rating DESC LIMIT 4\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you’re familiar with IMDb ratings, you know there are plenty of movies scoring above 8.3. The highest-rated title in our database is actually a series — Band of Brothers — with an impressive 9.6 rating. Once again, text embeddings perform poorly when it comes to sorting results.\n",
    "\n",
    "Let’s also evaluate a question that requires some sort of aggregation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "BGN72svBmfCO",
    "outputId": "d9e009e2-e63d-4f49-e24d-ac7d215e544a"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "plot: Ten television drama films, each one based on one of the Ten Commandments.\n",
      "title: Decalogue, The (Dekalog)\n",
      "year: 1989\n",
      "imdbRating: 9.2\n",
      "\n",
      "plot: A documentary which challenges former Indonesian death-squad leaders to reenact their mass-killings in whichever cinematic genres they wish, including classic Hollywood crime scenarios and lavish musical numbers.\n",
      "title: Act of Killing, The\n",
      "year: 2012\n",
      "imdbRating: 8.2\n",
      "\n",
      "plot: A meek Hobbit and eight companions set out on a journey to destroy the One Ring and the Dark Lord Sauron.\n",
      "title: Lord of the Rings: The Fellowship of the Ring, The\n",
      "year: 2001\n",
      "imdbRating: 8.8\n",
      "\n",
      "plot: While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.\n",
      "title: Lord of the Rings: The Two Towers, The\n",
      "year: 2002\n",
      "imdbRating: 8.7\n"
     ]
    }
   ],
   "source": [
    "pretty_print(neo4j_vector.similarity_search(\"How many movies are there?\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "vP_DtdvimpP1",
    "outputId": "36ad7fe3-067f-4eff-e956-27a8d71b1bf7"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'count(*)': 1000}]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "graph.query(\"\"\"MATCH (m:Target) RETURN count(*)\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The results are definitely not helpful here as we get returned four random movies. It’s virtually impossible to get from these random four movies to conclusion that there are a total of 1000 movies we tagged and embedded for this example\n",
    "\n",
    "So, what’s the solution? It’s straightforward: questions involving structured operations like filtering, sorting, and aggregation need tools specifically designed to operate with structured data.\n",
    "\n",
    "## Tools for structured data\n",
    "At the moment it seems that most people think about the text2query approach, where an LLM generates a database query to interact with a database based on the provided question and schema. For Neo4j specifically this is text2cypher, but there is also text2sql for SQL databases. However, it turns out in practice that it isn’t reliable and not robust enough for production use.\n",
    "\n",
    "![image](https://miro.medium.com/v2/resize:fit:1400/format:webp/0*01cgymtST8cG6b8C.png)\n",
    "\n",
    "You can use techniques like chain of thought, few-shot examples, or fine-tuning, but achieving high accuracy remains nearly impossible at this stage. The text2query approach works well for simple questions on straightforward database schemas, but that’s not the reality of production environments. To address this, we shift the complexity of generating database queries away from an LLM and treat it as a code problem where we generate database queries deterministically based on function inputs. The advantage is significantly improved robustness, though it comes at the cost of reduced flexibility. It’s better to narrow the scope of the RAG application and answer those questions accurately, rather than attempt to answer everything but do so inaccurately.\n",
    "\n",
    "Since we are generating database queries — in this case, Cypher statements — based on function inputs, we can leverage the tool capabilities of LLMs. In this process, the LLM populates the relevant parameters based on user input, while the function handles retrieving the necessary information. For this demonstration, we will first implement two tools: one for counting movies and another for listing them, and then create an LLM agent using LangGraph.\n",
    "\n",
    "## Tool for counting movies\n",
    "We will being by implementing a tool for counting movies based on predefined filters. First, we have to define what those filters are and describe to an LLM when and how to use them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def extract_param_name(filter: str) -> str:\n",
    "    # Regex to find parameters in the Cypher statement\n",
    "    pattern = r'\\$\\w+'\n",
    "    # Search for the first match\n",
    "    match = re.search(pattern, filter)\n",
    "    \n",
    "    # Output the first found parameter, if it exists\n",
    "    if match:\n",
    "        return match.group()[1:]\n",
    "    return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MovieCountInput(BaseModel):\n",
    "    min_year: Optional[int] = Field(\n",
    "        description=\"Minimum release year of the movies\"\n",
    "    )\n",
    "    max_year: Optional[int] = Field(\n",
    "        description=\"Maximum release year of the movies\"\n",
    "    )\n",
    "    min_rating: Optional[float] = Field(description=\"Minimum imdb rating\")\n",
    "    grouping_key: Optional[str] = Field(\n",
    "        description=\"The key to group by the aggregation\", enum=[\"year\"]\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LangChain offers several ways to define function inputs, but I prefer the Pydantic approach. In this example, we have three filters available to refine movie results: min_year, max_year, and min_rating. These filters are based on structured data and are all optional, as the user may choose to include any, all, or none of them. Additionally, we've introduced a grouping_key input that tells the function whether to group the count by a specific property. In this case, the only supported grouping is by year, as defined in the enumsection.\n",
    "\n",
    "Now let’s define the actual function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "id": "YmQl8-mQ0EJv"
   },
   "outputs": [],
   "source": [
    "@tool(\"movie-count\", args_schema=MovieCountInput)\n",
    "def movie_count(\n",
    "    min_year: Optional[int],\n",
    "    max_year: Optional[int],\n",
    "    min_rating: Optional[float],\n",
    "    grouping_key: Optional[str],\n",
    ") -> List[Dict]:\n",
    "    \"\"\"Calculate the count of movies based on particular filters\"\"\"\n",
    "\n",
    "    filters = [\n",
    "        (\"t.year >= $min_year\", min_year),\n",
    "        (\"t.year <= $max_year\", max_year),\n",
    "        (\"t.imdbRating >= $min_rating\", min_rating),\n",
    "    ]\n",
    "\n",
    "    # Create the parameters dynamically from function inputs\n",
    "    params = {\n",
    "        extract_param_name(condition): value\n",
    "        for condition, value in filters\n",
    "        if value is not None\n",
    "    }\n",
    "    where_clause = \" AND \".join(\n",
    "        [condition for condition, value in filters if value is not None]\n",
    "    )\n",
    "\n",
    "    cypher_statement = \"MATCH (t:Target) \"\n",
    "    if where_clause:\n",
    "        cypher_statement += f\"WHERE {where_clause} \"\n",
    "\n",
    "    return_clause = (\n",
    "        f\"t.`{grouping_key}`, count(t) AS movie_count\"\n",
    "        if grouping_key\n",
    "        else \"count(t) AS movie_count\"\n",
    "    )\n",
    "\n",
    "    cypher_statement += f\"RETURN {return_clause}\"\n",
    "\n",
    "    print(cypher_statement)  # Debugging output\n",
    "    return graph.query(cypher_statement, params=params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The movie_count function generates a Cypher query to count movies based on optional filters and grouping key. It begins by defining a list of filters with corresponding values provided as arguments. The filters are used to dynamically build the WHERE clause, which is responsible for applying the specified filtering conditions in the Cypher statement, including only those conditions where values are not None.\n",
    "\n",
    "The RETURN clause of the Cypher query is then constructed, either grouping by the provided grouping_key or simply counting the total number of movies. Finally, the function executes the query and returns the results.\n",
    "\n",
    "The function can be extended with more arguments and more involved logic as needed, but it’s important to ensure it remains clear so that an LLM can call it correctly and accurately.\n",
    "\n",
    "## Tool for listing movies\n",
    "Again, we have to start by defining arguments of the function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MovieListInput(BaseModel):\n",
    "    sort_by: str = Field(description=\"How to sort movies, can be one of either latest, rating\", enum=['latest', 'rating'])\n",
    "    k: Optional[int] = Field(description=\"Number of movies to return\")\n",
    "    description: Optional[str] = Field(description=\"Description of the movies\")\n",
    "    min_year: Optional[int] = Field(description=\"Minimum release year of the movies\")\n",
    "    max_year: Optional[int] = Field(description=\"Maximum release year of the movies\")\n",
    "    min_rating: Optional[float] = Field(description=\"Minimum imdb rating\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We keep the same three filters as in the movie count function, but we add the description argument. This argument lets us search and list movies based on their plot using vector similarity search. Just because we’re using structured tools and filters doesn’t mean we can’t incorporate text embedding and vector search methods. Since we don’t want to return all movies most of the time, we include an optional k input with a default value. Additionally, for listing, we want to sort the movies to return only the most relevant ones. In this case, we can sort them by rating or release year.\n",
    "\n",
    "Let’s now implement the function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "@tool(\"movie-list\", args_schema=MovieListInput)\n",
    "def movie_list(\n",
    "    sort_by: str = \"rating\",\n",
    "    k : int = 4,\n",
    "    description: Optional[str] = None,\n",
    "    min_year: Optional[int] = None,\n",
    "    max_year: Optional[int] = None,\n",
    "    min_rating: Optional[float] = None,\n",
    ") -> List[Dict]:\n",
    "    \"\"\"List movies based on particular filters\"\"\"\n",
    "\n",
    "    # Handle vector-only search when no prefiltering is applied\n",
    "    if description and not min_year and not max_year and not min_rating:\n",
    "        return neo4j_vector.similarity_search(description, k=k)\n",
    "\n",
    "    filters = [\n",
    "        (\"t.year >= $min_year\", min_year),\n",
    "        (\"t.year <= $max_year\", max_year),\n",
    "        (\"t.imdbRating >= $min_rating\", min_rating),\n",
    "    ]\n",
    "\n",
    "    # Create parameters dynamically from function arguments\n",
    "    params = {\n",
    "        key.split(\"$\")[1]: value for key, value in filters if value is not None\n",
    "    }\n",
    "    where_clause = \" AND \".join(\n",
    "        [condition for condition, value in filters if value is not None]\n",
    "    )\n",
    "\n",
    "    cypher_statement = \"MATCH (t:Target) \"\n",
    "    if where_clause:\n",
    "        cypher_statement += f\"WHERE {where_clause} \"\n",
    "\n",
    "    # Add the return clause with sorting\n",
    "    cypher_statement += \" RETURN t.title AS title, t.year AS year, t.imdbRating AS rating ORDER BY \"\n",
    "\n",
    "    # Handle sorting logic based on description or other criteria\n",
    "    if description:\n",
    "        cypher_statement += (\n",
    "            \"vector.similarity.cosine(t.embedding, $embedding) DESC \"\n",
    "        )\n",
    "        params[\"embedding\"] = embedding.embed_query(description)\n",
    "    elif sort_by == \"rating\":\n",
    "        cypher_statement += \"t.imdbRating DESC \"\n",
    "    else:  # sort by latest year\n",
    "        cypher_statement += \"t.year DESC \"\n",
    "\n",
    "    cypher_statement += \" LIMIT toInteger($limit)\"\n",
    "    params[\"limit\"] = k or 4\n",
    "\n",
    "    print(cypher_statement)  # Debugging output\n",
    "    data = graph.query(cypher_statement, params=params)\n",
    "    return data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This function retrieves a list of movies based on multiple optional filters: description, year range, minimum rating, and sorting preferences. If only a description is given with no other filters, it performs a vector index similarity search to find relevant movies. When additional filters are applied, the function constructs a Cypher query to match movies based on the specified criteria, such as release year and IMDb rating, combining them with an optional description-based similarity. The results are then sorted by either the similarity score, IMDb rating, or year, and limited to k movies.\n",
    "\n",
    "## Putting it all together as a LangGraph agent\n",
    "We will implement a straightforward ReAct agent using LangGraph.\n",
    "\n",
    "The agent consists of an LLM and tools step. As we interact with the agent, we will first call the LLM to decide if we should use tools. Then we will run a loop:\n",
    "\n",
    "* If the agent said to take an action (i.e. call tool), we’ll run the tools and pass the results back to the agent\n",
    "* If the agent did not ask to run tools, we will finish (respond to the user)\n",
    "\n",
    "The code implementation is as straightforward as it gets. First we bind the tools to the LLM and define the assistant step."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "llm = ChatOpenAI(model='gpt-4-turbo')\n",
    "\n",
    "tools = [movie_count, movie_list]\n",
    "llm_with_tools = llm.bind_tools(tools)\n",
    "\n",
    "# System message\n",
    "sys_msg = SystemMessage(content=\"You are a helpful assistant tasked with finding and explaining relevant information about movies.\")\n",
    "\n",
    "# Node\n",
    "def assistant(state: MessagesState):\n",
    "   return {\"messages\": [llm_with_tools.invoke([sys_msg] + state[\"messages\"])]}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we define the LangGraph flow."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Graph\n",
    "builder = StateGraph(MessagesState)\n",
    "\n",
    "# Define nodes: these do the work\n",
    "builder.add_node(\"assistant\", assistant)\n",
    "builder.add_node(\"tools\", ToolNode(tools))\n",
    "\n",
    "# Define edges: these determine how the control flow moves\n",
    "builder.add_edge(START, \"assistant\")\n",
    "builder.add_conditional_edges(\n",
    "    \"assistant\",\n",
    "    # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools\n",
    "    # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END\n",
    "    tools_condition,\n",
    ")\n",
    "builder.add_edge(\"tools\", \"assistant\")\n",
    "react_graph = builder.compile()\n",
    "\n",
    "# Show\n",
    "display(Image(react_graph.get_graph(xray=True).draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We define two nodes in the LangGraph and link them with a conditional edge. If a tool is called, the flow is directed to the tools; otherwise, the results are sent back to the user.\n",
    "\n",
    "Let’s now test our agent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What are the some movies about a girl meeting her hero?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  movie-list (call_LbXxiTRavkwgPArhDpFi9N1g)\n",
      " Call ID: call_LbXxiTRavkwgPArhDpFi9N1g\n",
      "  Args:\n",
      "    description: girl meets her hero\n",
      "    sort_by: rating\n",
      "    k: 5\n",
      "    min_year: None\n",
      "    max_year: None\n",
      "    min_rating: None\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: movie-list\n",
      "\n",
      "[Document(metadata={'movieId': '57504', 'tmdbId': '14069', 'imdbVotes': 33731, 'runtime': 98, 'countries': ['Japan'], 'imdbId': '0808506', 'url': 'https://themoviedb.org/movie/14069', 'released': '2006-07-15', 'languages': ['Japanese'], 'poster': 'https://image.tmdb.org/t/p/w440_and_h660_face/7oNeWrsaiMGxCrvkWjQ56y6JN85.jpg', 'revenue': 3800000}, page_content='\\nplot: A high-school girl acquires the ability to time travel.\\ntitle: Girl Who Leapt Through Time, The (Toki o kakeru shôjo)\\nyear: 2006\\nimdbRating: 7.9'), Document(metadata={'movieId': '66934', 'tmdbId': '14301', 'imdbVotes': 33031, 'runtime': 42, 'countries': ['USA'], 'imdbId': '1227926', 'url': 'https://themoviedb.org/movie/14301', 'released': '2008-07-15', 'languages': ['English']}, page_content=\"\\nplot: An aspiring supervillain must balance his career and his pursuit of a beautiful do-gooder.\\ntitle: Dr. Horrible's Sing-Along Blog\\nyear: 2008\\nimdbRating: 8.7\"), Document(metadata={'budget': 165000000, 'movieId': '115617', 'tmdbId': '177572', 'imdbVotes': 253901, 'runtime': 102, 'countries': ['USA'], 'imdbId': '2245084', 'url': 'https://themoviedb.org/movie/177572', 'released': '2014-11-07', 'languages': ['English'], 'poster': 'https://image.tmdb.org/t/p/w440_and_h660_face/2mxS4wUimwlLmI1xp6QW6NSU361.jpg', 'revenue': 657827828}, page_content='\\nplot: The special bond that develops between plus-sized inflatable robot Baymax, and prodigy Hiro Hamada, who team up with a group of friends to form a band of high-tech heroes.\\ntitle: Big Hero 6\\nyear: 2014\\nimdbRating: 7.9'), Document(metadata={'movieId': '26903', 'tmdbId': '37797', 'imdbVotes': 28132, 'runtime': 111, 'countries': ['Japan'], 'imdbId': '0113824', 'url': 'https://themoviedb.org/movie/37797', 'released': '1995-07-15', 'languages': ['Japanese', ' English'], 'poster': 'https://image.tmdb.org/t/p/w440_and_h660_face/7oQgBscjpNLgOCaU1nBVSjWdIrC.jpg'}, page_content='\\nplot: A love story between a girl who loves reading books, and the boy who has previously checked out all of the library books she chooses.\\ntitle: Whisper of the Heart (Mimi wo sumaseba)\\nyear: 1995\\nimdbRating: 8.0'), Document(metadata={'budget': 23000000, 'movieId': '106920', 'tmdbId': '152601', 'imdbVotes': 333347, 'runtime': 126, 'countries': ['USA'], 'imdbId': '1798709', 'url': 'https://themoviedb.org/movie/152601', 'released': '2014-01-10', 'languages': ['English'], 'poster': 'https://image.tmdb.org/t/p/w440_and_h660_face/yk4J4aewWYNiBhD49WD7UaBBn37.jpg', 'revenue': 47351251}, page_content=\"\\nplot: A lonely writer develops an unlikely relationship with his newly purchased operating system that's designed to meet his every need.\\ntitle: Her\\nyear: 2013\\nimdbRating: 8.0\")]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Here are some movies about a girl meeting her hero or forming a significant relationship with a central character:\n",
      "\n",
      "1. **The Girl Who Leapt Through Time (Toki o kakeru shôjo)** (2006)\n",
      "   - **Plot**: A high-school girl acquires the ability to time travel.\n",
      "   - **IMDb Rating**: 7.9\n",
      "   - ![The Girl Who Leapt Through Time](https://image.tmdb.org/t/p/w440_and_h660_face/7oNeWrsaiMGxCrvkWjQ56y6JN85.jpg)\n",
      "   - [More Info](https://themoviedb.org/movie/14069)\n",
      "\n",
      "2. **Dr. Horrible's Sing-Along Blog** (2008)\n",
      "   - **Plot**: An aspiring supervillain must balance his career and his pursuit of a beautiful do-gooder.\n",
      "   - **IMDb Rating**: 8.7\n",
      "   - [More Info](https://themoviedb.org/movie/14301)\n",
      "\n",
      "3. **Big Hero 6** (2014)\n",
      "   - **Plot**: The special bond that develops between plus-sized inflatable robot Baymax, and prodigy Hiro Hamada, who team up with a group of friends to form a band of high-tech heroes.\n",
      "   - **IMDb Rating**: 7.9\n",
      "   - ![Big Hero 6](https://image.tmdb.org/t/p/w440_and_h660_face/2mxS4wUimwlLmI1xp6QW6NSU361.jpg)\n",
      "   - [More Info](https://themoviedb.org/movie/177572)\n",
      "\n",
      "4. **Whisper of the Heart (Mimi wo sumaseba)** (1995)\n",
      "   - **Plot**: A love story between a girl who loves reading books, and the boy who has previously checked out all of the library books she chooses.\n",
      "   - **IMDb Rating**: 8.0\n",
      "   - ![Whisper of the Heart](https://image.tmdb.org/t/p/w440_and_h660_face/7oQgBscjpNLgOCaU1nBVSjWdIrC.jpg)\n",
      "   - [More Info](https://themoviedb.org/movie/37797)\n",
      "\n",
      "5. **Her** (2013)\n",
      "   - **Plot**: A lonely writer develops an unlikely relationship with his newly purchased operating system that's designed to meet his every need.\n",
      "   - **IMDb Rating**: 8.0\n",
      "   - ![Her](https://image.tmdb.org/t/p/w440_and_h660_face/yk4J4aewWYNiBhD49WD7UaBBn37.jpg)\n",
      "   - [More Info](https://themoviedb.org/movie/152601)\n",
      "\n",
      "These films explore various interpretations and narratives around the theme of a girl meeting her hero or a pivotal character that changes her life.\n"
     ]
    }
   ],
   "source": [
    "messages = [\n",
    "    HumanMessage(\n",
    "        content=\"What are the some movies about a girl meeting her hero?\"\n",
    "    )\n",
    "]\n",
    "messages = react_graph.invoke({\"messages\": messages})\n",
    "for m in messages[\"messages\"]:\n",
    "    m.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MATCH (t:Target) WHERE t.year >= $min_year AND t.year <= $max_year  RETURN t.title AS title, t.year AS year, t.imdbRating AS rating ORDER BY vector.similarity.cosine(t.embedding, $embedding) DESC  LIMIT toInteger($limit)\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What are the movies from the 90s about a girl meeting her hero?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  movie-list (call_cG8GU8i3xciFG40vo9GSvNql)\n",
      " Call ID: call_cG8GU8i3xciFG40vo9GSvNql\n",
      "  Args:\n",
      "    description: girl meeting her hero\n",
      "    min_year: 1990\n",
      "    max_year: 1999\n",
      "    sort_by: latest\n",
      "    k: None\n",
      "    min_rating: None\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: movie-list\n",
      "\n",
      "[{\"title\": \"Whisper of the Heart (Mimi wo sumaseba)\", \"year\": 1995, \"rating\": 8.0}, {\"title\": \"Léon: The Professional (a.k.a. The Professional) (Léon)\", \"year\": 1994, \"rating\": 8.6}, {\"title\": \"Breaking the Waves\", \"year\": 1996, \"rating\": 7.9}, {\"title\": \"Before Sunrise\", \"year\": 1995, \"rating\": 8.1}]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Here are some movies from the 90s that involve a storyline about a girl meeting her hero:\n",
      "\n",
      "1. **Whisper of the Heart (Mimi wo sumaseba)** (1995) - IMDB Rating: 8.0\n",
      "   - In this animated film, a young girl meets a boy who helps her follow her dreams of becoming a writer, serving as an inspirational hero in her journey.\n",
      "\n",
      "2. **Léon: The Professional (a.k.a. The Professional) (Léon)** (1994) - IMDB Rating: 8.6\n",
      "   - A young girl forms an unlikely friendship with a hitman, who becomes her protector and mentor as she seeks vengeance for her family's murder.\n",
      "\n",
      "3. **Breaking the Waves** (1996) - IMDB Rating: 7.9\n",
      "   - The story follows a woman whose life is transformed by her deep love and faith, leading her to perform extreme acts in the name of what she perceives as love.\n",
      "\n",
      "4. **Before Sunrise** (1995) - IMDB Rating: 8.1\n",
      "   - This romantic drama explores the connection between a young woman and a man she meets on a train in Europe, who inspires her to explore life and love over the course of an evening.\n",
      "\n",
      "These films each showcase unique encounters with hero-like figures through various genres and narratives.\n"
     ]
    }
   ],
   "source": [
    "messages = [\n",
    "    HumanMessage(\n",
    "        content=\"What are the movies from the 90s about a girl meeting her hero?\"\n",
    "    )\n",
    "]\n",
    "messages = react_graph.invoke({\"messages\": messages})\n",
    "for m in messages[\"messages\"]:\n",
    "    m.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MATCH (t:Target) WHERE t.year >= $min_year AND t.year <= $max_year AND t.imdbRating >= $min_rating RETURN count(t) AS movie_count\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "How many movies are from the 90s have the rating higher than 9.1?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  movie-count (call_fLRtZRwlPic0w5w111wJIrcP)\n",
      " Call ID: call_fLRtZRwlPic0w5w111wJIrcP\n",
      "  Args:\n",
      "    min_year: 1990\n",
      "    max_year: 1999\n",
      "    min_rating: 9.1\n",
      "    grouping_key: None\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: movie-count\n",
      "\n",
      "[{\"movie_count\": 3}]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "There are 3 movies from the 1990s with an IMDb rating higher than 9.1.\n"
     ]
    }
   ],
   "source": [
    "messages = [\n",
    "    HumanMessage(\n",
    "        content=\"How many movies are from the 90s have the rating higher than 9.1?\"\n",
    "    )\n",
    "]\n",
    "messages = react_graph.invoke({\"messages\": messages})\n",
    "for m in messages[\"messages\"]:\n",
    "    m.pretty_print()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MATCH (t:Target)  RETURN t.title AS title, t.year AS year, t.imdbRating AS rating ORDER BY t.imdbRating DESC  LIMIT toInteger($limit)\n",
      "MATCH (t:Target) WHERE t.year >= $min_year RETURN t.`year`, count(t) AS movie_count\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "How many were movies released per year made after the highest rated movie?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  movie-list (call_YWmf0oSM1JYWSfZpChqswH1r)\n",
      " Call ID: call_YWmf0oSM1JYWSfZpChqswH1r\n",
      "  Args:\n",
      "    sort_by: rating\n",
      "    k: 1\n",
      "    description: None\n",
      "    min_year: None\n",
      "    max_year: None\n",
      "    min_rating: None\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: movie-list\n",
      "\n",
      "[{\"title\": \"Band of Brothers\", \"year\": 2001, \"rating\": 9.6}]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  movie-count (call_C9HCC1iEBmYkGOuIeINEFssj)\n",
      " Call ID: call_C9HCC1iEBmYkGOuIeINEFssj\n",
      "  Args:\n",
      "    min_year: 2002\n",
      "    max_year: None\n",
      "    min_rating: None\n",
      "    grouping_key: year\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: movie-count\n",
      "\n",
      "[{\"t.`year`\": 2002, \"movie_count\": 18}, {\"t.`year`\": 2003, \"movie_count\": 26}, {\"t.`year`\": 2004, \"movie_count\": 27}, {\"t.`year`\": 2005, \"movie_count\": 17}, {\"t.`year`\": 2006, \"movie_count\": 24}, {\"t.`year`\": 2007, \"movie_count\": 25}, {\"t.`year`\": 2008, \"movie_count\": 27}, {\"t.`year`\": 2009, \"movie_count\": 22}, {\"t.`year`\": 2010, \"movie_count\": 24}, {\"t.`year`\": 2011, \"movie_count\": 12}, {\"t.`year`\": 2012, \"movie_count\": 21}, {\"t.`year`\": 2013, \"movie_count\": 21}, {\"t.`year`\": 2014, \"movie_count\": 29}, {\"t.`year`\": 2015, \"movie_count\": 20}, {\"t.`year`\": 2016, \"movie_count\": 4}]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "The highest rated movie is \"Band of Brothers,\" released in 2001. Here is the number of movies released each year after 2001:\n",
      "\n",
      "- 2002: 18 movies\n",
      "- 2003: 26 movies\n",
      "- 2004: 27 movies\n",
      "- 2005: 17 movies\n",
      "- 2006: 24 movies\n",
      "- 2007: 25 movies\n",
      "- 2008: 27 movies\n",
      "- 2009: 22 movies\n",
      "- 2010: 24 movies\n",
      "- 2011: 12 movies\n",
      "- 2012: 21 movies\n",
      "- 2013: 21 movies\n",
      "- 2014: 29 movies\n",
      "- 2015: 20 movies\n",
      "- 2016: 4 movies\n",
      "\n",
      "These counts reflect how many movies were released each year following the year \"Band of Brothers\" was released.\n"
     ]
    }
   ],
   "source": [
    "messages = [\n",
    "    HumanMessage(\n",
    "        content=\"How many were movies released per year made after the highest rated movie?\"\n",
    "    )\n",
    "]\n",
    "messages = react_graph.invoke({\"messages\": messages})\n",
    "for m in messages[\"messages\"]:\n",
    "    m.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "colab": {
   "authorship_tag": "ABX9TyNUA4K/Z70dicSLph78HEbl",
   "include_colab_link": true,
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
